#!/usr/bin/env python
# lidd.py vi:ts=4:sw=4:expandtab:
#
# LDAP Information Distribution Suite
# Authors:
#       Will Barton <wbb4@opendarwin.org>
#       Landon Fuller <landonf@opendarwin.org
#
# Copyright (c) 2005 Three Rings Design, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. Neither the name of the copyright owner nor the names of contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import getopt, sys, ldap
import os, signal, logging
from twisted.internet import reactor
import ZConfig

import lids
from lids import ldaputils, daemon, plugin

def usage():
    print "%s: [-h] [-f config file]" % sys.argv[0]
    print "    -h             Print usage (this message)"
    print "    -f <config>    Use configuration file"

def main():
    conf_file = None
    
    try:
        opts,args = getopt.getopt(sys.argv[1:], "hvf:")
    except getopt.GetoptError:
        usage()
        sys.exit(2)

    for opt,arg in opts:
        if opt == "-h":
            usage()
            sys.exit()
        if opt == "-f":
            conf_file = arg

    if (conf_file == None):
        usage()
        sys.exit(1)

    # Load our configuration schema
    schema = ZConfig.loadSchema(lids.CONFIG_SCHEMA)
    try:
        config, handler = ZConfig.loadConfig(schema, conf_file)
    except ZConfig.ConfigurationError, e:
        print "Configuration Error: %s" % e
        sys.exit(1)

    # Daemonize
    daemonize(config)

    # Acquire our logger
    logger = logging.getLogger(lids.LOG_NAME)

    # Set up the daemon context 
    try:
        conn = ldaputils.Connection(config.LDAP.uri)
        conn.simple_bind(config.LDAP.binddn, config.LDAP.password)
    except ldap.INVALID_CREDENTIALS:
        logger.critical("Authentication failed for server: %s", config.LDAP.uri)
        sys.exit(1)
    except ldap.NO_SUCH_OBJECT:
        logger.critical("Authentication failed for server: %s, no such object. Is your Bind DN correct?", config.LDAP.uri)
        sys.exit(1)
    except ldap.SERVER_DOWN:
        logger.critical("Could not contact server: %s", config.LDAP.uri)
        sys.exit(1)

    ctx = daemon.Context(conn)

    # Load all service helpers
    for service in config.Service:
        options = {}

        # Set up service options
        for opt in service.Option:
            options[opt.getSectionName()] = opt.value

        try:
            hc = plugin.HelperController(service.helper, service.frequency, service.searchbase,
                    service.searchfilter, service.groupbase, service.groupfilter, options)
        except plugin.LIDSPluginError, e:
            logger.critical("Error initializing service '%s': %s", service.getSectionName(), e)
            sys.exit(1)

        ctx.addHelper(service.getSectionName(), hc)

    # Add our daemon context to the runloop
    ctx.start()

    # Fire up the reactor
    reactor.run()

def daemonize(config):
    """ Detach a process from the terminal and run it as a daemon """
    # Close all open files.
    try:
        maxfd = os.sysconf("SC_OPEN_MAX")
    except (AttributeError, ValueError):
        maxfd = 1024       # default maximum

    # Avoid closing stdout/stdin/stderr so we can still print a log
    # initialization error message ...
    stdin = sys.stdin.fileno()
    stdout = sys.stdout.fileno()
    stderr = sys.stdout.fileno()

    for fd in range(0, maxfd):
        try:
            if (fd != stdout and fd != stderr and fd != stdin):
                os.close(fd)
        except OSError:
            # Ignore EBADF
            pass

    # Set up logging
    try:
        config.Logging()
    except Exception, e:
        print "Log initialization failed: %s" % e
        sys.exit(1)

    # Redirect stdin, stdout, and stderr to /dev/null
    null = os.open('/dev/null', os.O_RDWR)
    os.dup2(null, stdin)
    os.dup2(null, stdout)
    os.dup2(null, stderr)

    if (null > 2):
        os.close(null)

    logger = logging.getLogger(lids.LOG_NAME)
    logger.info("LIDS Starting up...")

    # Detach the daemon
    try:
        pid = os.fork()
    except OSError, e:
        logger.critical("An OSError occurred in daemonize(): %s", e)
        sys.exit(1)

    if pid == 0:
        # become the session leader
        os.setsid()

        # ignore SIGHUP, since children are sent SIGHUP when the parent
        # terminates
        signal.signal(signal.SIGHUP, signal.SIG_IGN)

        try:
            # second child to prevent zombies
            pid = os.fork()
        except OSError, e:
            logger.critical("An OSError occurred in daemonize(): %s", e)
            sys.exit(1)

        if pid == 0:
            # Make sure we don't keep any directory in use
            os.chdir("/")
            os.umask(0)
        else:
            os._exit(0)
    else:
        os._exit(0)

    return 0

if __name__ == "__main__":
    main()
